import asyncio
import logging
from pymodbus.client import AsyncModbusTcpClient
from pymodbus.framer.rtu_framer import ModbusRtuFramer
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.pdu import ExceptionResponse
import asyncio
from pymodbus.constants import Endian
from rs485tomqtt.device import Device
from ..mqtt_client import MqttClient


ENDIAN_DICT = {
    "BIG": Endian.BIG,
    "LITTLE": Endian.LITTLE
}
PREPARE_FUNC = {
    "mapping": lambda arg, value: arg.get(str(value), value) if isinstance(arg, dict) else value,
    "mult": lambda arg, value: arg * value,
    "add": lambda arg, value: arg + value,
}
_log = logging.getLogger("modbus")


# modbus 实例
class ModbusComm:
    def __init__(self, conf: dict, mqttClient: MqttClient, deviceDic: dict[str, Device]):
        self._conf = conf
        self.mqttClient = mqttClient
        self.mqttClient.register_listener(self.on_message)
        self.host = conf["host"]
        self.port = conf["port"]
        self.deviceDic = deviceDic
        self.deviceItemData = {}
        self.modbus_client: AsyncModbusTcpClient = None
        self.initialized = False

    # 异步初始化，需要在asyncio.run之后执行
    async def init_async(self):
        self.write_queue = asyncio.Queue()
        self.device_locks = {
            name: asyncio.Lock() for name in self.deviceDic
        }
        self.write_task = asyncio.create_task(self._process_write_queue())
        self.initialized = True

    # 收到消息
    def on_message(self, topicName, payload: dict):
        if self.modbus_client.connected:
            if topicName in self.deviceDic:
                device = self.deviceDic[topicName]
                for key, value in payload.items():
                    if key in device.rpc:
                        self.write_queue.put_nowait(
                            (device, device.rpc[key], value))
        else:
            _log.error(f"串口服务器没有连接")

    # 队列处理写入
    async def _process_write_queue(self):
        _log.info(f"启动队列写入监听")
        while True:
            device, setting, rpcValue = await self.write_queue.get()
            await self.add_task(setting, device, rpcValue=rpcValue)

    # 通过功能码读取数据
    async def add_task(self, setting: dict, dev: Device, rpcValue=None):
        func = setting.get("funcode")
        if func == 1:
            await self.read_coils(setting, dev)
        elif func == 2:
            await self.read_coil_input(setting, dev)
        elif func == 3:
            await self.read_registers(setting, dev)
        elif func == 4:
            await self.read_input(setting, dev)
        elif func == 5:
            await self.write_coil(setting, dev, rpcValue)
        elif func == 6:
            await self.write_register(setting, dev, rpcValue)
        elif func is None:
            _log.error("读取数据错误")

    def _decode_value(self, data_type, decoder: BinaryPayloadDecoder):
        if data_type == "int32":
            value = decoder.decode_32bit_int()
        elif data_type == "int16":
            value = decoder.decode_16bit_int()
        elif data_type == "uint32":
            value = decoder.decode_32bit_uint()
        elif data_type == "uint16":
            value = decoder.decode_16bit_uint()
        elif data_type == "int8":
            value = decoder.decode_8bit_int()
        elif data_type == "uint8":
            value = decoder.decode_8bit_uint()
        elif data_type == "float16":
            value = decoder.decode_16bit_float()
        elif data_type == "float32":
            value = decoder.decode_32bit_float()
        elif data_type == "float64":
            value = decoder.decode_64bit_float()
        elif data_type == "binary":
            value = decoder.decode_16bit_uint()
        elif data_type == "juji_combination32":
            value = eval(decoder["expression"], decoder["param"])
        elif data_type == "hex":
            decimal_number = decoder.decode_16bit_uint()
            hex_string = hex(decimal_number)
            value = hex_string[2:].zfill(4)  # 去掉十六进制字符串的前缀"0x"
        else:
            raise NotImplementedError(data_type)
        return value

    # 功能码1读取数据
    async def read_coils(self, setting, dev: Device):
        addr = int(setting["address"])
        count = setting["count"]
        try:
            async with self.device_locks[dev.name]:
                rr = await asyncio.wait_for(self.modbus_client.read_coils(addr, count, slave=int(dev.addr)), timeout=2)
            if rr.isError() or isinstance(rr, ExceptionResponse):
                _log.error(
                    f"查询错误: {dev.name}, slaveid: {dev.addr},address: {setting['address']}")
            else:
                for datapoint in setting["mapping"]:
                    if datapoint["type"] != "bool":
                        raise NotImplementedError("Only support bool!")
                    else:
                        if datapoint.get("point"):
                            point = datapoint["point"]
                            if point > len(rr.bits):
                                raise OverflowError("Point too large!")
                            value = rr.bits[point]
                        else:
                            value = rr.bits[0]
                        value = 1 if value else 0
                        if datapoint.get("mp"):
                            value = datapoint.get("mp")[value]
                            if value == "none":
                                continue
                    self.deviceItemData[dev.name
                                        ][datapoint['key']] = value
        except Exception as e:
            _log.error(f"{dev.name}查询失败:{e}")

    # 功能码2读取数据
    async def read_coil_input(self, setting, dev: Device):
        addr = int(setting["address"])
        count = setting["count"]
        try:
            async with self.device_locks[dev.name]:
                rr = await asyncio.wait_for(self.modbus_client.read_discrete_inputs(addr, count, slave=int(dev.addr)), timeout=2)
            if rr.isError() or isinstance(rr, ExceptionResponse):
                _log.error(
                    f"查询错误: {dev.name}, slaveid: {dev.addr},address: {setting['address']}")
            else:
                for datapoint in setting["mapping"]:
                    if datapoint["type"] != "bool":
                        raise NotImplementedError("Only support bool!")
                    else:
                        if datapoint.get("point"):
                            point = datapoint["point"]
                            if point > len(rr.bits):
                                raise OverflowError("Point too large!")
                            value = rr.bits[point]
                        else:
                            value = rr.bits[0]
                        value = 1 if value else 0
                        if datapoint.get("mp"):
                            temp = datapoint.get("mp")[value]
                            if temp == "none":
                                continue
                        self.deviceItemData[dev.name
                                            ][datapoint['key']] = value
        except Exception as e:
            _log.error(f"{dev.name}查询失败:{e}")

    # 功能码3读取数据
    async def read_registers(self, setting, dev: Device):
        addr = int(setting["address"])
        count = setting["count"]
        try:
            async with self.device_locks[dev.name]:
                rr = await asyncio.wait_for(self.modbus_client.read_holding_registers(addr, count, slave=int(dev.addr)), timeout=2)
            if rr.isError() or isinstance(rr, ExceptionResponse):
                _log.error(
                    f"查询错误: {dev.name}, slaveid: {dev.addr},address: {setting['address']}")
            else:
                for datapoint in setting["mapping"]:
                    try:
                        offset = int(datapoint["addr"]) - addr
                        registers = rr.registers[offset:]
                    except Exception as e:
                        _log.error("offset error %s", dev['name'])
                        return
                    decoder = BinaryPayloadDecoder.fromRegisters(
                        registers, byteorder=ENDIAN_DICT[dev.byteOrder], wordorder=ENDIAN_DICT[dev.wordOrder])
                    data_type = datapoint["type"]
                    value = self._decode_value(data_type, decoder)
                    if datapoint.get("scale"):
                        value = value * datapoint["scale"]
                        if datapoint.get("offset"):
                            value = value + datapoint["offset"]
                    if datapoint.get("prepare"):
                        pre: dict = datapoint.get("prepare")
                        value = PREPARE_FUNC[pre["type"]](
                            pre.get("arg", None), value)
                    self.deviceItemData[dev.name][datapoint['key']] = value
        except Exception as e:
            _log.error(f"{dev.name}查询失败:{e}")

    # 功能码4读取数据
    async def read_input(self, setting, dev: Device):
        addr = int(setting["address"])
        count = setting["count"]
        try:
            async with self.device_locks[dev.name]:
                rr = await asyncio.wait_for(self.modbus_client.read_input_registers(addr, count, slave=int(dev.addr)), timeout=2)
            if rr.isError() or isinstance(rr, ExceptionResponse):
                _log.error(
                    f"查询错误: {dev.name}, slaveid: {dev.addr},address: {setting['address']}")
            else:
                # [240, 119, 0, 28672, 500, 0, 71, 0, 234, 0, 0, 0, 0, 0, 6, 20471, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 20471]
                # _log.debug("rr: %s, ip: %s, port: %s,name: %s",
                #            rr.registers, self.host, self.port, dev.name)
                for datapoint in setting["mapping"]:
                    try:
                        offset = int(datapoint["addr"]) - addr
                        registers = rr.registers[offset:]
                    except Exception as e:
                        _log.error("offset error %s", dev.name)
                        return
                    decoder = BinaryPayloadDecoder.fromRegisters(
                        registers, byteorder=ENDIAN_DICT[dev.byteOrder], wordorder=ENDIAN_DICT[dev.wordOrder])
                    data_type = datapoint["type"]
                    value = self._decode_value(data_type, decoder)
                    if datapoint.get("scale"):
                        value = value * datapoint["scale"]
                        if datapoint.get("offset"):
                            value = value + datapoint["offset"]
                    self.deviceItemData[dev.name][datapoint['key']] = value
        except Exception as e:
            _log.error(f"{dev.name}查询失败:{e}")

    # 功能码5写入数据
    async def write_coil(self, setting: dict, dev: Device, rpcValue):
        addr = int(setting["address"])
        data_type = setting["type"]
        if data_type != "bool":
            raise NotImplementedError("Only support bool!")
        value = rpcValue
        if setting.get("prepare"):
            pre: dict = setting["prepare"]
            value = PREPARE_FUNC[pre["type"]](
                pre.get("arg", None), rpcValue)
        try:
            rr = await asyncio.wait_for(self.modbus_client.write_coil(addr, value, slave=int(dev.addr)), timeout=4)
            if rr.isError() or isinstance(rr, ExceptionResponse):
                _log.error(
                    f"写入数据失败: {dev.name},address: {addr}")
            else:
                _log.info(f"{dev.name}写入数据成功:{setting['key']}:{rpcValue}")
                payload = {}
                payload[setting["key"]] = rpcValue
                self.mqttClient.send_msg(
                    dev.topic + dev.name, payload)
        except Exception as e:
            _log.error(f"{dev.name}写入数据失败:{e}")

    # 功能码6写入数据
    async def write_register(self, setting: dict, dev: Device, rpcValue):
        addr = int(setting["address"])
        data_type = setting["type"]
        if data_type != "int16":
            raise NotImplementedError("Only support int16!")
        value = rpcValue
        if setting.get("prepare"):
            pre: dict = setting["prepare"]
            value = PREPARE_FUNC[pre["type"]](
                pre.get("arg", None), rpcValue)
        try:
            async with self.device_locks[dev.name]:
                rr = await asyncio.wait_for(self.modbus_client.write_register(addr, value, slave=int(dev.addr)), timeout=4)
            if rr.isError() or isinstance(rr, ExceptionResponse):
                _log.error(
                    f"写入数据失败: {dev.name}, slaveid: {int(dev.addr)},address: {addr}")
            else:
                _log.info(f"{dev.name}写入数据成功:{setting['key']}:{rpcValue}")
                payload = {}
                payload[setting["key"]] = rpcValue
                self.mqttClient.send_msg(
                    dev.topic + dev.name, payload)
        except Exception as e:
            _log.error(f"{dev.name}写入数据失败:{e}")

    # 循环查询设备
    async def loop_src(self):
        for dev in self.deviceDic.values():
            self.deviceItemData = {}
            self.deviceItemData[dev.name] = {}
            topic = dev.topic
            for setting in dev.ts:
                await self.add_task(setting, dev)
            self.mqttClient.send_msg(
                topic + dev.name, self.deviceItemData[dev.name])

    # 读取数据
    async def run_async_client(self):
        if not self.initialized:
            await self.init_async()
        if self.modbus_client is None:
            self.modbus_client = AsyncModbusTcpClient(
                host=self.host,
                port=self.port,
                framer=ModbusRtuFramer,
                timeout=3,
                retries=1,
                reconnect_delay=1,
                reconnect_delay_max=3,
            )
            await self.connectModbus()
        else:
            await self.connectModbus()

    # 连接modbus
    async def connectModbus(self):
        if self.modbus_client.connected:
            await self.loop_src()
        else:
            try:
                await self.modbus_client.connect()
                if self.modbus_client.connected:
                    _log.info(f"串口服务器连接成功{self.host}:{self.port}")
                    await self.loop_src()
                else:
                    _log.error(f"串口服务器连接失败{self.host}:{self.port}")
            except Exception as e:
                _log.error(f"初始化连接异常{self.host}:{self.port}:{e}")
